package com.zhaokuner.bind;

import org.springframework.beans.BeanUtils;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.env.PropertySources;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Validator;

import java.beans.PropertyDescriptor;
import java.util.*;

/**
 * Validate some {@link Properties} (or optionally {@link PropertySources}) by binding
 * them to an object of a specified type and then optionally running a {@link Validator}
 * over it.
 *
 * @param <T> the target type
 * @author Dave Syer
 */
public class PropertiesConfigurationFactory<T> implements FactoryBean<T>, InitializingBean {


    private static final char[] EXACT_DELIMITERS = {'_', '.', '['};

    private static final char[] TARGET_NAME_DELIMITERS = {'_', '.'};

    private PropertySources propertySources;

    private final T target;

    private String targetName;

    private boolean hasBeenBound = false;

    private boolean resolvePlaceholders = true;

    private boolean ignoreUnknownFields = true;

    /**
     * Create a new {@link PropertiesConfigurationFactory} instance.
     *
     * @param target the target object to bind too
     * @see #PropertiesConfigurationFactory(Class)
     */
    public PropertiesConfigurationFactory(T target) {
        Assert.notNull(target, "target must not be null");
        this.target = target;
    }

    /**
     * Create a new {@link PropertiesConfigurationFactory} instance.
     *
     * @param type the target type
     * @see #PropertiesConfigurationFactory(Class)
     */
    @SuppressWarnings("unchecked")
    public PropertiesConfigurationFactory(Class<?> type) {
        Assert.notNull(type, "type must not be null");
        this.target = (T) BeanUtils.instantiateClass(type);
    }

    /**
     * Set the target name.
     *
     * @param targetName the target name
     */
    public void setTargetName(String targetName) {
        this.targetName = targetName;
    }

    /**
     * Set the property sources.
     *
     * @param propertySources the property sources
     */
    public void setPropertySources(PropertySources propertySources) {
        this.propertySources = propertySources;
    }

    @Override
    public Class<?> getObjectType() {
        if (this.target == null) {
            return Object.class;
        }
        return this.target.getClass();
    }

    @Override
    public boolean isSingleton() {
        return true;
    }

    @Override
    public T getObject() throws Exception {
        if (!this.hasBeenBound) {
            bindPropertiesToTarget();
        }
        return this.target;
    }


    @Override
    public void afterPropertiesSet() throws Exception {
        bindPropertiesToTarget();
    }

    public void bindPropertiesToTarget() throws BindException {
        Assert.state(this.propertySources != null, "PropertySources should not be null");
        try {

            this.hasBeenBound = true;
            doBindPropertiesToTarget();
        } catch (BindException ex) {
            throw ex;
        }
    }


    private void doBindPropertiesToTarget() throws BindException {
        RelaxedDataBinder dataBinder = (this.targetName != null
                ? new RelaxedDataBinder(this.target, this.targetName)
                : new RelaxedDataBinder(this.target));

        dataBinder.setAutoGrowCollectionLimit(Integer.MAX_VALUE);
        Iterable<String> relaxedTargetNames = getRelaxedTargetNames();
        Set<String> names = getNames(relaxedTargetNames);
        PropertyValues propertyValues = getPropertySourcesPropertyValues(names,
                relaxedTargetNames);
        dataBinder.bind(propertyValues);

        checkForBindingErrors(dataBinder);
    }

    private Iterable<String> getRelaxedTargetNames() {
        return (this.target != null && StringUtils.hasLength(this.targetName)
                ? new RelaxedNames(this.targetName) : null);
    }

    private Set<String> getNames(Iterable<String> prefixes) {
        Set<String> names = new LinkedHashSet<String>();
        if (this.target != null) {
            PropertyDescriptor[] descriptors = BeanUtils
                    .getPropertyDescriptors(this.target.getClass());
            for (PropertyDescriptor descriptor : descriptors) {
                String name = descriptor.getName();
                if (!name.equals("class")) {
                    RelaxedNames relaxedNames = RelaxedNames.forCamelCase(name);
                    if (prefixes == null) {
                        for (String relaxedName : relaxedNames) {
                            names.add(relaxedName);
                        }
                    } else {
                        for (String prefix : prefixes) {
                            for (String relaxedName : relaxedNames) {
                                names.add(prefix + "." + relaxedName);
                                names.add(prefix + "_" + relaxedName);
                            }
                        }
                    }
                }
            }
        }
        return names;
    }

    private PropertyValues getPropertySourcesPropertyValues(Set<String> names,
                                                            Iterable<String> relaxedTargetNames) {
        PropertyNamePatternsMatcher includes = getPropertyNamePatternsMatcher(names,
                relaxedTargetNames);
        return new PropertySourcesPropertyValues(this.propertySources, names, includes,
                this.resolvePlaceholders);
    }

    private void checkForBindingErrors(RelaxedDataBinder dataBinder)
            throws BindException {
        BindingResult errors = dataBinder.getBindingResult();
        if (errors.hasErrors()) {
            throw new BindException(errors);
        }
    }


    private PropertyNamePatternsMatcher getPropertyNamePatternsMatcher(Set<String> names,
                                                                       Iterable<String> relaxedTargetNames) {
        if (this.ignoreUnknownFields && !isMapTarget()) {
            // Since unknown fields are ignored we can filter them out early to save
            // unnecessary calls to the PropertySource.
            return new DefaultPropertyNamePatternsMatcher(EXACT_DELIMITERS, true, names);
        }
        if (relaxedTargetNames != null) {
            // We can filter properties to those starting with the target name, but
            // we can't do a complete filter since we need to trigger the
            // unknown fields check
            Set<String> relaxedNames = new HashSet<String>();
            for (String relaxedTargetName : relaxedTargetNames) {
                relaxedNames.add(relaxedTargetName);
            }
            return new DefaultPropertyNamePatternsMatcher(TARGET_NAME_DELIMITERS, true,
                    relaxedNames);
        }
        // Not ideal, we basically can't filter anything
        return PropertyNamePatternsMatcher.ALL;
    }

    private boolean isMapTarget() {
        return this.target != null && Map.class.isAssignableFrom(this.target.getClass());
    }
}
